<?php

//
// Edit History:
//
//  Last $Author: munroe $
//  Last Modified: $Date: 2006/04/04 21:31:37 $
//
//  Dick Munroe (munroe@csworks.com) 28-Feb-2006
//      Initial version created
//
//  Dick Munroe (munroe@csworks.com) 12-Mar-2006
//      Default behavior for sourceNotEmpty is to add any left over variables to the
//      IPN data.
//
//  Dick Munroe (munroe@csworks.com) 14-Mar-2006
//      Add method for handling alternate payment status.
//      Get the error paths on the processing to set the proper return status.
//
//  Dick Munroe (munroe@csworks.com) 15-Mar-2006
//      If the url for the testing mode is the null string, don't interact with
//      paypal, just say that things verified.
//
//  Dick Munroe (munroe@csworks.com) 16-Mar-2006
//      The constructor needs to check for exactly NULL for default values.
//
//  Dick Munroe (munroe@csworks.com) 20-Mar-2006
//      Add step to processing for verifying the item details.
//
//  Dick Munroe (munroe@csworks.com) 22-Mar-2006
//      The test for default arguments in the constructor had to be stronger.
//
//  Dick Munroe (munroe@csworks.com) 23-Mar-2006
//      (Blush) The interaction with Paypal wasn't being sent as a POST request.
//      Things would work as long as not TOO much data needed to be returned
//      (generally the case).
//
//  Dick Munroe (munroe@csworks.com) 23-Mar-2006
//      Redesign the processNotification routine to make it easier to reuse
//      this class as a base class for PDT processing as well as IPN processing.
//
//	Dick Munroe (munroe@csworks.com) 04-Apr-2006
//		Add interface to return contents of the IPN data.
//

/**
 * @author Dick Munroe <munroe@csworks.com>
 * @copyright copyright @ 2006 by Dick Munroe, Cottage Software Works, Inc.
 * @license http://www.csworks.com/publications/ModifiedNetBSD.html
 * @version 1.1.1
 * @package dm.paypal
 * @example ./example.php
 *
 * This is derived from a paypal IPN class written originally by Herve Foucher
 * <Herve.Foucher@helio.org> and published through phpclasses.org under the GPL.
 * Herve appears to no longer support this package so I'm updating the support
 * to 2005 and redesigning the internal structure to allow a substantially
 * more object oriented approach to the whole problem.
 *
 * This requires the PHP cURL module to be installed and also requires the following
 * additional classes from phpclasses.org:
 *
 *   cURL:                        http://munroe.users.phpclasses.org/browse/package/1988.html
 *
 * These have to be installed on your PHP search path or within the directory containing
 * the Paypal IPN classes in a directory named curl.
 *
 * Much of the complexity of the provided source is due to the way I organized the IPN
 * data parsing.  I'm really into understanding where various things group and the
 * Paypal developers are really into making that pretty much as difficult as possible
 * to figure out.  So my attempts to group and understand everything provided by
 * Paypal in the IPN documentation are more complex than many much simpler $_POST
 * processing schemes.  On the other hand, they are more robust and make it simple
 * to tell when Paypal has introduced new stuff into the protocol.
 */

include_once('class.paypalIPNData.php') ;
include_once('curl/class.curl.php') ;

/*
 * The default URLs for the live and sandbox versions of Paypal.
 */

define ("PAYPAL_SECURED_URL", "https://www.paypal.com/cgi-bin/webscr");
define ("PAYPAL_SANDBOX_SECURED_URL", "https://www.sandbox.paypal.com/cgi-bin/webscr") ;

/**
 * This is an abstract class that has to be overridden to provide useful application
 * level processing.
 */

class paypalIPNBase

{
    var $m_proxyURL;

    var $m_proxyUserPassword;

    /**
     * @access protected
     * @var object  cURL interface object.  Primarily intended to be used for debugging as it could
     *                     easily be made local to httpPost.
     */

    var $m_curl ;

    /**
     * @access protected
     * @var array  The contents of the paypal data provided by the invocation of the IPN at the local site.
     */

    var $m_data ;

    /**
     * @desc The local site's valid receiver email.
     * @access protected.
     */

    var $m_receiverEmail ;

    /**
     * @desc the URL of the Paypal verification processor.
     * @access protected.
     */

    var $m_paypalURL ;

    /**
     * @desc the URL of the Paypal Sandbox verification processor.
     * @access protected.
     */

    var $m_sandboxURL ;

    /**
     * Constructor
     *
     * @param string $thePaypalURL The URL of the paypal verification URL.
     * @param string $theSandboxURL The URL of the paypal sandbox verification URL.
     * @return void
     * @access public
     */

    function paypalIPNBase($theReceiverEmail, $thePaypalURL = NULL, $theSandboxURL = NULL)
    {
        $this->m_receiverEmail = $theReceiverEmail ;

        if ($thePaypalURL === NULL)
        {
            $this->m_paypalURL = PAYPAL_SECURED_URL ;
        }
        else
        {
            $this->m_paypalURL = $thePaypalURL ;
        }

        if ($theSandboxURL === NULL)
        {
            $this->m_sandboxURL = PAYPAL_SANDBOX_SECURED_URL ;
        }
        else
        {
            $this->m_sandboxURL = $theSandboxURL ;
        }

        $this->m_proxyURL = null;

        $this->m_proxyUserPassword = null;
    }

    /*
     * This function must be overridden in order to provide actual
     * IPN preprocessing.  The source of the IPN data may be modified
     * freely at this point to inject any additional fields into the
     * request.
     *
     * @desc Preprocess the IPN.
     * @param reference to array $theSource for the IPN data.
     * @access public
     */

    function preprocess(&$theSource)
    {
        trigger_error("preprocess must be overridden", E_USER_ERROR) ;
    }

    /*
     * This function must be overridden in order to provide actual
     * IPN postprocessing.
     *
     * @desc Postprocess the IPN.
     * @param object $theIPNData the IPN data provided by Paypal.
     * @param integer $theStatus the HTTP status of the Paypal IPN processing.
     * @return void
     * @access public
     */

    function postprocess(&$theIPN, $theStatus)
    {
        trigger_error("postprocess must be overridden", E_USER_ERROR) ;
    }

    /**
     * @desc Get the IPN data from the source.
     * @param array by reference the source from which IPN data is to be extracted.
     * @access private
     * @return boolean true if the source is empty after processing.
     */

    function getData(&$theSource)
    {
        /*
         * Parse the IPN data out of the source and inject the cmd that will need
         * to be sent during verification.
         */

        $this->m_data =& new paypalIPNData($theSource, array('cmd' => '_notify-validate')) ;
        return empty($theSource) ;
    }

    /**
     * @desc Get the IPN data.
     * @access public
     * @return object by reference the IPN data object.
     */

    function getIPN()
    {
		return $this->m_data ;
    }

    /*
     * @desc Do the application level processing associated with the source not fully processed.
     *
     * By default, the sourceNotEmpty method moves any keys and data not already in the
     * IPN data to the IPN data.  This protects against changes in the IPN standard over time
     * but MAY cause problems should variables injected by other sources crop up.
     *
     * I advise overriding this method at the application level and logging the "missing"
     * variables so that the site developer can modify the underlying IPN data classes to
     * parse for the new variables and notify the maintainer of this package.
     *
     * @param object $theIPN the IPN data provided by Paypal.
     * @param array $theSource The remaining data not processed into the IPN data.
     * return boolean True if the IPN processing is to abort, false otherwise.
     * @access protected
     */

    function sourceNotEmpty(&$theIPN, &$theSource)
    {
        $theIPN->addData($theSource) ;
        return false ;
    }

    /**
     * @desc Check for a match with the receiver_email
     * @param string theReceiverEmail
     * @return boolean.
     * @access private
     */

    function checkReceiver($theReceiverEmail)
    {
        return $theReceiverEmail == $this->m_receiverEmail ;
    }

    /**
     * @desc Check for a verified response from Paypal.
     * @param string theResponse
     * @return boolean.
     * @access private
     */

    function checkVerified($theResponse)
    {
        return $theResponse == "VERIFIED" ;
    }

    /**
     * @desc Check for a payment complete from Paypal.
     * @param string theResponse
     * @return boolean.
     * @access private
     */

    function paymentComplete($thePaymentStatus)
    {
        return $thePaymentStatus == "Completed" ;
    }
    
    /**
     * @desc Process IPNs with payment status other than Completed.
     * @param object by reference The parsed IPN data.
     * @return boolean True if the alternate status was processed without error.
     * @access protected
     */
     
    function alternatePaymentStatus(&$theIPN)
    {
        trigger_error("alternatePaymentStatus must be overridden", E_USER_ERROR) ;
    }

    /**
     * @desc Do the application level checking for a unique transaction Id.
     * @param string theTransactionId.
     * @return boolean True if the transaction id is unique.
     * @access protected
     */

    function checkTransactionId($theTransactionId)
    {
        trigger_error("checkTransactionId must be overridden", E_USER_ERROR) ;
    }

    /**
     * @desc Do the application level validation of the item details.
     * @param object theIPN
     * @return boolean True if the item details match.
     * @access protected
     */
     
    function validateItem(&$theIPN)
    {
        trigger_error("validateItem must be overriden", E_USER_ERROR) ;
    }
    
    /**
     * @desc Do the application level processing of the payment.
     *
     * At this point the IPN data is fully verified and anything that needs to
     * be done to record data should be taken care of.
     *
     * @param object theIPN
     * @return boolean True if the payment was processed correctly
     * @access protected
     */

    function processPayment(&$theIPN)
    {
        trigger_error("processPayment must be overridden", E_USER_ERROR) ;
    }

    /**
     * This function calls helper functions that model each piece of the IPN process as documented in
     * Paypal Hacks, chapter 7.  The overall process is:
     *
     *  1. Connect to the application specific preprocessing hook.
     *  2. Verify the IPN with Paypal.
     *  3. If the IPN is verified, Check for a completed transaction.
     *  4. If the transaction was completed, check that the transaction ID isn't a duplicate.
     *  5. If the transaction ID is unique, check that the receiver email address is for the local site.
     *  6. If the receiver email address matches, check that the item details presented make sense.
     *  7. If the item details make sense, process the payment.
     *
     * @desc Process an IPN received in a $_POST array.
     * @param array $theSource [by reference] the arguments passed by Paypal as part of the IPN process.
     * @return boolean False if the IPN wasn't processed.
     * @access public
     */

    function processNotification (&$theSource)
    {
        $this->preprocess($theSource) ;

        $theReturnStatus = TRUE ;

        if (!$this->getData($theSource))
        {
            $theReturnStatus = !$this->sourceNotEmpty($this->m_data, $theSource) ;
        }

        $theIPN = $this->m_data ;

        if ($theReturnStatus)
        {
            if (($theIPN->getData('test_ipn') !== NULL) && ($theIPN->getData('test_ipn') == '1'))
            {
                $theURL = $this->m_sandboxURL ;
            }
            else
            {
                $theURL = $this->m_paypalURL ;
            }

            if ($theURL != '')
            {
                $theReturnStatus = $this->httpPost($theURL, $theIPN) ;
            }
            else
            {
                /*
                 * For debugging purposes, if the URL for interacting with is not provided,
                 * then force a VERIFIED response.
                 */
                 
                $theReturnStatus = "VERIFIED" ;
            }

            $theReturnStatus = $this->_processNotification($theReturnStatus, $theIPN) ;
        }

        $this->postprocess($theIPN, $theReturnStatus) ;
        
        return $theReturnStatus ;
    }

    /**
     * This function calls helper functions that model each piece of the IPN process as documented in
     * Paypal Hacks, chapter 7.  The overall process is:
     *
     *  1. If the IPN is verified, Check for a completed transaction.
     *  2. If the transaction was completed, check that the transaction ID isn't a duplicate.
     *  3. If the transaction ID is unique, check that the receiver email address is for the local site.
     *  4. If the receiver email address matches, check that the item details presented make sense.
     *  5. If the item details make sense, process the payment.
     *
     * @desc Drive the payment process
     * @param $theReturnStatus The status string returned by paypal.
     * @param array $theIPN [by reference] the IPN data extracted from Paypal.
     * @return boolean False if the IPN wasn't processed.
     * @access public
     */

    function _processNotification($theReturnStatus, &$theIPN)
    {
        if ($this->checkVerified($theReturnStatus))
        {
            if ($this->paymentComplete($theIPN->getData('payment_status')))
            {
                if ($this->checkTransactionId($theIPN->getData('txn_id')))
                {
                    if ($this->checkReceiver($theIPN->getData('receiver_email')))
                    {
                        if ($this->validateItem($theIPN))
                        {
                            if ($this->processPayment($theIPN))
                            {
                                $theReturnStatus = true ;
                            }
                            else
                            {
                                $theReturnStatus = false ;
                            }
                        }
                        else
                        {
                            $theReturnStatus = false ;
                        }
                    }
                    else
                    {
                        $theReturnStatus = false ;
                    }
                }
                else
                {
                    $theReturnStatus = false ;
                }
            }
            else
            {
                $theReturnStatus = $this->alternatePaymentStatus($theIPN) ;
            }
        }
        else
        {
            $theReturnStatus = false ;
        }

        return $theReturnStatus ;
    }
    
    /**
     * @desc Set the proxy gateway options.
     * @param string $theProxyURL the url for the proxy server.
     * @param string $$theProxyUserPassword the user and password necessary for the proxy server.
     * @return void
     * @access public
     */

    function setProxyOptions ($theProxyURL, $theProxyUserPassword)
    {
        $this->m_proxyURL = $theProxyURL;
        $this->m_proxyUserPassword = $theProxyUserPassword;
    }

    /*
     * @desc Post the IPN notification back to Paypal for verification.
     * @param string $url the Paypal verification URL.
     * @param object $theIPN [by reference] the IPN notification data.
     * @access private
     */

    function httpPost ($url, &$theIPN)
    {
        /*
         * Technically the m_curl variable should be local to here.  It's being kept
         * in object context so that debugging will be a bit easier.
         */

        $this->m_curl = new cURL() ;

        // Notification if transfered into a urlencoded string

        $thePostString = $theIPN->asPostString() ;

        $this->m_curl->setopt(CURLOPT_URL, $url);
        $this->m_curl->setopt(CURLOPT_POST, 1) ;
        $this->m_curl->setopt(CURLOPT_RETURNTRANSFER,1);
        $this->m_curl->setopt(CURLOPT_SSL_VERIFYHOST, 2) ;
        $this->m_curl->setopt(CURLOPT_SSL_VERIFYPEER, FALSE) ;
        $this->m_curl->setopt(CURLOPT_POSTFIELDS, $thePostString);

        // If you need to go through a proxy server, see set_proxy_options

        if (!is_null($this->m_proxyURL) && !is_null($this->m_proxyUserPassword))
        {
            if (preg_match("/[^:]+:[0-9]+/", $this->m_proxyURL) &&
                preg_match("/([^:]+):.*/", $this->m_proxyUserPassword, $matches))
            {
                    $this->m_curl->setopt(CURLOPT_PROXY, $this->m_proxyURL);
                    $this->m_curl->setopt(CURLOPT_PROXYUSERPWD, $this->m_proxyUserPassword);
            }
            else
            {
                trigger_error("Can't set proxy information", E_USER_ERROR) ;
                return false ;
            }
        }

        $thePaypalResponse = $this->m_curl->exec();

        $this->m_curl->close();

        if(preg_match("/(VERIFIED|INVALID)/", $thePaypalResponse, $matches))
        {
            $response = $matches[1];
        }
        else
        {
            $response = FALSE ;
        }
        return $response;
    }
}
?>
